Skip to content

feat: 유저 프로필 조회 API 연동#30

Merged
easyhooon merged 3 commits intodevelopfrom
BOOK-92-feature/#28
Jul 7, 2025
Merged

feat: 유저 프로필 조회 API 연동#30
easyhooon merged 3 commits intodevelopfrom
BOOK-92-feature/#28

Conversation

@easyhooon
Copy link
Copy Markdown
Contributor

@easyhooon easyhooon commented Jul 5, 2025

🔗 관련 이슈

📙 작업 설명

  • 유저 프로필 조회 API 연동
  • 유저 정보(닉네임, 이메일)를 내서재 화면내에 load

🧪 테스트 내역 (선택)

  • 주요 기능 정상 동작 확인
  • 브라우저/기기에서 동작 확인
  • 엣지 케이스 테스트 완료
  • 기존 기능 영향 없음

📸 스크린샷 또는 시연 영상 (선택)

기능 미리보기
기능 설명

💬 추가 설명 or 리뷰 포인트 (선택)

  • Loading Initial Data 관련해서 더 좋은 방법이 있을지 의견 주시면 감사하겠습니다. 저도 더 고민해보려구요!
  • 더 고민해본 결과, API를 통해 받아온 데이터를 state로 관리하는 경우(이후 변경될 가능성이 있는 경우), 기존 compose에서 제공하는 SideEffect API 인 produceState와 유사한 Circuit의 produceRetainedState를 사용하여 non-Compose 상태를 Compose 상태로 변환하여 사용하는 것이 괜찮을 것 같다고 생각했슴니다.(관련해서 예시 코드 첨부하도록 하겠습니다.)
  • rememberEffect는 skydoves님의 compose-effects에서 지원하는 API로 LaunchedEffect와 매우 흡사하나, 내부에 코루틴 스코프가 존재하지않아, 매번 코루틴 스코프를 생성하는 비용을 아낄수있습니다. LaunchedEffect를 사용할때 내부에서 suspend 함수를 호출하지 않는 경우, flow를 collect하지 않는 경우등에 사용하면 좋을 것 같아 채택을 해보았습니다
  • 이후 내 서재 화면 시안 나오면, 현재 작업내역은 지우도록 하겠습니다

Summary by CodeRabbit

  • 신규 기능

    • 사용자 프로필(닉네임, 이메일) 조회 및 표시 기능이 추가되었습니다. 라이브러리 화면에서 사용자 정보를 확인할 수 있습니다.
  • UI 개선

    • 라이브러리 화면에 사용자 닉네임과 이메일이 표시됩니다.
  • 버그 수정

    • 프로필 조회 실패 시 오류 메시지 표시 및 로그인 필요 시 로그인 화면으로 이동하도록 개선되었습니다.
  • 기타

    • 새로운 라이브러리 및 내부 구조 개선이 반영되었습니다.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jul 5, 2025

Walkthrough

유저 프로필 조회 API를 연동하고, 조회된 이메일과 닉네임을 '내 서재' 화면에 표시하는 기능이 추가되었습니다. 이를 위해 데이터 모델, 네트워크 응답, 레포지토리, 프레젠터, UI 등 전반적으로 코드가 확장 및 수정되었습니다.

Changes

파일/경로 그룹 변경 요약
build-logic/src/main/kotlin/AndroidFeatureConventionPlugin.kt
gradle/libs.versions.toml
compose-effects 라이브러리 의존성 추가
core/model/src/main/kotlin/com/ninecraft/booket/core/model/UserProfileModel.kt UserProfileModel 데이터 클래스 추가
core/network/src/main/kotlin/com/ninecraft/booket/core/network/response/UserProfileResponse.kt UserProfileResponse 데이터 클래스(@serializable) 추가
core/network/src/main/kotlin/com/ninecraft/booket/core/network/service/AuthService.kt AuthService에 getUserProfile() API 추가
core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/UserRepository.kt UserRepository 인터페이스 추가
core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/di/RepositoryModule.kt
core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultUserRepository.kt
UserRepository DI 바인딩 및 DefaultUserRepository 구현체 추가
core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/mapper/ResponseToModel.kt UserProfileResponse → UserProfileModel 변환 매퍼 추가
feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryPresenter.kt UserRepository 주입, 유저 프로필 조회 및 상태 관리, 닉네임/이메일 상태 추가, 로그아웃 관련 변수명 정리
feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryScreen.kt LibraryScreen.State에 닉네임/이메일 필드 추가, UI에서 해당 정보 노출, SideEffect 핸들링 위치 조정 및 프리뷰 수정

Sequence Diagram(s)

sequenceDiagram
    participant UI as LibraryScreen
    participant Presenter as LibraryPresenter
    participant UserRepo as UserRepository (DefaultUserRepository)
    participant AuthSvc as AuthService

    UI->>Presenter: present() 호출 (초기 컴포즈)
    activate Presenter
    Presenter->>UserRepo: getUserProfile()
    activate UserRepo
    UserRepo->>AuthSvc: getUserProfile()
    activate AuthSvc
    AuthSvc-->>UserRepo: UserProfileResponse 반환
    UserRepo-->>Presenter: Result<UserProfileModel> 반환
    Presenter-->>UI: 상태 업데이트 (nickname, email)
    deactivate Presenter
Loading

Assessment against linked issues

Objective Addressed Explanation
유저 프로필 조회 API 연동 (#28)
내 서재 탭내 임시 텍스트 구성(이메일, 닉네임) (#28)

Poem

(⁎˃ᆺ˂) 。・::・゚★,。・::・゚☆

내 서재에 토끼가 깡총 뛰어와
닉네임, 이메일 살짝 보여주네
API도 연결, 화면도 반짝
프로필 정보에 모두가 깜짝!
오늘도 코드밭에 당근 한 아름~

૮₍ ˶ᵔ ᵕ ᵔ˶ ₎ა 🥕

✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryScreen.kt (1)

85-95: UI 구조 개선을 고려해보세요.

사용자 프로필 정보 표시 기능이 잘 구현되어 있지만, 중첩된 Column 구조가 다소 복잡합니다.

다음과 같이 구조를 단순화할 수 있습니다:

-            Column(
-                modifier = Modifier.fillMaxSize(),
-                horizontalAlignment = Alignment.CenterHorizontally,
-                verticalArrangement = Arrangement.Center,
-            ) {
-                Text(text = "내 서재")
-                Spacer(modifier = Modifier.height(16.dp))
-                Text(text = state.nickname)
-                Spacer(modifier = Modifier.height(16.dp))
-                Text(text = state.email)
-            }
+            Column(
+                modifier = Modifier.align(Alignment.Center),
+                horizontalAlignment = Alignment.CenterHorizontally,
+            ) {
+                Text(text = "내 서재")
+                Spacer(modifier = Modifier.height(16.dp))
+                Text(text = state.nickname)
+                Spacer(modifier = Modifier.height(16.dp))
+                Text(text = state.email)
+            }
feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryPresenter.kt (1)

38-66: 유저 프로필 조회 함수가 잘 구현되었습니다.

로딩 상태 관리, 에러 핸들링, 성공/실패 케이스 처리가 모두 적절히 구현되어 있습니다. handleException 유틸리티 함수를 사용하여 일관된 에러 처리를 하고 있어 좋습니다.

다만 함수명을 loadUserProfile()이나 fetchUserProfile()로 변경하는 것을 고려해보세요. 현재 getUserProfile()은 데이터를 반환하는 것처럼 보이지만 실제로는 사이드 이펙트를 수행하는 함수입니다.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9d45323 and 88319c5.

📒 Files selected for processing (11)
  • build-logic/src/main/kotlin/AndroidFeatureConventionPlugin.kt (1 hunks)
  • core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/UserRepository.kt (1 hunks)
  • core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/di/RepositoryModule.kt (2 hunks)
  • core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/mapper/ResponseToModel.kt (1 hunks)
  • core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultUserRepository.kt (1 hunks)
  • core/model/src/main/kotlin/com/ninecraft/booket/core/model/UserProfileModel.kt (1 hunks)
  • core/network/src/main/kotlin/com/ninecraft/booket/core/network/response/UserProfileResponse.kt (1 hunks)
  • core/network/src/main/kotlin/com/ninecraft/booket/core/network/service/AuthService.kt (1 hunks)
  • feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryPresenter.kt (4 hunks)
  • feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryScreen.kt (5 hunks)
  • gradle/libs.versions.toml (2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (4)
build-logic/src/main/kotlin/AndroidFeatureConventionPlugin.kt (1)
build-logic/src/main/kotlin/com/ninecraft/booket/convention/Dependencies.kt (1)
  • implementation (6-8)
core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultUserRepository.kt (1)
core/common/src/main/kotlin/com/ninecraft/booket/core/common/utils/RunSuspendCatching.kt (1)
  • runSuspendCatching (16-30)
feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryScreen.kt (1)
feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/HandleLibrarySideEffects.kt (1)
  • HandleLibrarySideEffects (8-28)
feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryPresenter.kt (1)
core/common/src/main/kotlin/com/ninecraft/booket/core/common/utils/HandleException.kt (1)
  • handleException (9-36)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: ci-build
🔇 Additional comments (21)
gradle/libs.versions.toml (1)

18-18: compose-effects 라이브러리 버전 최신 및 보안 취약점 없음 확인

  • skydoves/compose-effects 최신 릴리스 태그가 0.1.1임을 확인했습니다.
  • GitHub 및 공개 보안 데이터베이스 검색 결과 해당 버전에 대한 알려진 취약점이 보고되지 않았습니다.

위 의존성 추가를 승인합니다.

build-logic/src/main/kotlin/AndroidFeatureConventionPlugin.kt (1)

27-27: 의존성 추가 잘 구현됨

compose-effects 라이브러리가 Android Feature Convention Plugin에 적절히 추가되었습니다. 기존 의존성 패턴과 일관성을 유지하고 있습니다.

core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/mapper/ResponseToModel.kt (2)

4-4: 임포트 추가 적절함

UserProfileModel과 UserProfileResponse 임포트가 새로운 기능에 필요한 타입들을 적절히 추가했습니다.

Also applies to: 6-6


15-22: 매퍼 함수 구현이 올바름

UserProfileResponse를 UserProfileModel로 변환하는 확장 함수가 깔끔하게 구현되었습니다. 모든 필드가 적절히 매핑되어 있고, 기존 LoginResponse.toModel() 함수와 일관된 패턴을 따르고 있습니다.

core/network/src/main/kotlin/com/ninecraft/booket/core/network/service/AuthService.kt (2)

3-4: 필요한 임포트 추가됨

UserProfileResponse와 GET 어노테이션 임포트가 새로운 API 메서드에 필요한 타입들을 적절히 추가했습니다.


11-12: 인증 토큰 전송 방식 확인 완료

  • TokenInterceptor.kt에서 chain.request().newBuilder().addHeader("Authorization", "Bearer $accessToken") 로 헤더를 자동 추가
  • NetworkModule.kt의 AuthOkHttpClient에 TokenInterceptor가 등록되어 AuthService 호출에 자동 적용
  • TokenAuthenticator.kt에서도 리프레시 후 새 토큰으로 Authorization 헤더를 갱신 처리

위 구현으로 getUserProfile() 요청 시 Bearer 토큰이 정상 전달되므로 추가 수정은 필요 없습니다.

core/model/src/main/kotlin/com/ninecraft/booket/core/model/UserProfileModel.kt (1)

3-8: 로깅 검색 결과 없음: UserProfileModel 관련 로깅이 발견되지 않았습니다.

rg 명령어로 UserProfileModel이 로그 호출에서 사용되는지 검사했으나 결과가 없습니다.
프로젝트에서 다른 로깅 라이브러리나 확장 메서드를 사용 중인지, 혹은 커스텀 래퍼를 통해 로그를 남기고 있지는 않은지 수동으로 확인해 주세요.

• UserProfileModel을 로그로 기록하는 코드가 존재하지 않음
• 다른 로깅 추상화(예: SLF4J 래퍼, AOP 기반 로깅 등) 사용 여부 확인 필요

core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/UserRepository.kt (1)

1-7: 깔끔한 레포지토리 인터페이스 구현입니다.

리포지토리 패턴을 올바르게 구현했으며, Result<T>suspend 함수를 사용하여 비동기 에러 핸들링을 적절히 지원하고 있습니다.

core/network/src/main/kotlin/com/ninecraft/booket/core/network/response/UserProfileResponse.kt (1)

6-16: 직렬화 설정이 올바르게 구성된 응답 데이터 클래스입니다.

@Serializable@SerialName 어노테이션을 적절히 사용하여 JSON 직렬화를 지원하고 있습니다.

모든 필드가 non-null String으로 정의되어 있는데, 실제 API 스펙에서 일부 필드(예: provider)가 nullable할 가능성이 있는지 확인해보세요.

core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/di/RepositoryModule.kt (2)

4-6: 적절한 의존성 주입 설정입니다.

새로운 import가 올바르게 추가되었습니다.


21-23: 일관성 있는 바인딩 설정입니다.

기존 AuthRepository 바인딩과 동일한 패턴을 따라 @Binds@Singleton 어노테이션을 적절히 사용하고 있습니다.

core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultUserRepository.kt (1)

9-15: 깔끔한 레포지토리 구현입니다.

의존성 주입, 에러 핸들링, 데이터 변환이 적절히 구현되어 있습니다. runSuspendCatching을 사용하여 코루틴 안전한 에러 핸들링을 제공하고 있습니다.

feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryScreen.kt (3)

35-36: 적절한 상태 확장입니다.

사용자 프로필 정보를 표시하기 위한 필드가 적절히 추가되었으며, 기본값 설정도 올바릅니다.


57-61: 사이드 이펙트 처리 위치가 개선되었습니다.

HandleLibrarySideEffects를 최상단으로 이동하여 관심사 분리가 더 명확해졌습니다.


131-132: 프리뷰 데이터가 적절히 추가되었습니다.

개발 및 디자인 검토를 위한 샘플 데이터가 적절히 제공되고 있습니다.

feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryPresenter.kt (6)

10-10: 새로운 의존성 import가 적절히 추가되었습니다.

유저 프로필 조회 기능 구현을 위한 UserRepository와 성능 최적화를 위한 RememberedEffect import가 올바르게 추가되었습니다.

Also applies to: 13-13


26-27: 의존성 주입이 적절히 개선되었습니다.

기존 AuthRepository의 네이밍을 authRepository로 명확히 하고, 새로운 UserRepository 의존성을 추가하여 관심사를 적절히 분리했습니다.


35-36: 상태 변수가 적절히 추가되었습니다.

rememberRetained를 사용하여 nicknameemail 상태를 관리하는 것이 올바른 접근입니다. 빈 문자열로 초기화하여 API 호출 전까지 적절한 기본값을 제공합니다.


68-70: RememberedEffect 사용이 적절합니다.

PR 목표에서 언급한 대로 LaunchedEffect 대신 RememberedEffect를 사용하여 성능 최적화를 달성했습니다. 컴포저블이 처음 구성될 때 한 번만 유저 프로필을 로드하는 것이 올바른 접근입니다.


82-82: 변수명 변경이 일관되게 적용되었습니다.

기존 로그아웃 로직에서 repositoryauthRepository로 변경하여 새로운 네이밍 규칙과 일관성을 유지했습니다.

Also applies to: 84-84


112-113: 반환 상태에 새로운 프로필 정보가 적절히 추가되었습니다.

nicknameemail 상태를 반환하여 UI에서 사용할 수 있도록 했습니다. 이는 유저 프로필 표시 기능을 완성하는 데 필요한 변경사항입니다.

@easyhooon
Copy link
Copy Markdown
Contributor Author

easyhooon commented Jul 5, 2025

produceRetainedState를 사용한다면...예시

LibraryPresenter.kt

class LibraryPresenter @AssistedInject constructor(
    @Assisted private val navigator: Navigator,
    private val authRepository: AuthRepository,
    private val userRepository: UserRepository,
) : Presenter<LibraryScreen.State> {

    @Composable
    override fun present(): LibraryScreen.State {
        val scope = rememberCoroutineScope()
        var sideEffect by rememberRetained { mutableStateOf<LibraryScreen.SideEffect?>(null) }
        
        val userProfileState by produceRetainedState<LibraryScreen.UserProfileState>(
            initialValue = LibraryScreen.UserProfileState.Loading
        ) {
            userRepository.getUserProfile()
                .onSuccess { user ->
                    value = LibraryScreen.UserProfileState.Success(
                        nickname = user.nickname,
                        email = user.email
                    )
                }
                .onFailure { exception ->
                    handleException(
                        exception = exception,
                        onServerError = { message ->
                            value = LibraryScreen.UserProfileState.Error(message)
                        },
                        onNetworkError = { message ->
                            value = LibraryScreen.UserProfileState.Error(message)
                        },
                        onLoginRequired = {
                            navigator.resetRoot(LoginScreen)
                        },
                    )
                }
        }

        fun handleEvent(event: LibraryScreen.Event) {
            when (event) {
                is LibraryScreen.Event.InitSideEffect -> {
                    sideEffect = null
                }

                is LibraryScreen.Event.OnLogoutButtonClick -> { 
                    // ...
                }
            }
        }

        return LibraryScreen.State(
            userProfile = userProfileState,
            sideEffect = sideEffect,
            eventSink = ::handleEvent,
        )
    }

    @CircuitInject(LibraryScreen::class, ActivityRetainedComponent::class)
    @AssistedFactory
    fun interface Factory {
        fun create(navigator: Navigator): LibraryPresenter
    }
}

LibraryScreen.kt

@Parcelize
data object LibraryScreen : Screen {
    sealed interface UserProfileState {
        data object Loading : UserProfileState
        data class Success(val nickname: String, val email: String) : UserProfileState
        data class Error(val message: String) : UserProfileState
    }

    data class State(
        val userProfile: UserProfileState = UserProfileState.Loading,
        val sideEffect: SideEffect? = null,
        val eventSink: (Event) -> Unit,
    ) : CircuitUiState

    sealed interface SideEffect {
        data class ShowToast(val message: String) : SideEffect
    }

    sealed interface Event : CircuitUiEvent {
        data object InitSideEffect : Event
        data object OnLogoutButtonClick : Event
    }
}

@CircuitInject(LibraryScreen::class, ActivityRetainedComponent::class)
@Composable
internal fun Library(
    state: LibraryScreen.State,
    modifier: Modifier = Modifier,
) {
    HandleLibrarySideEffects(
        state = state,
        eventSink = state.eventSink,
    )

    Column(
        modifier = modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center,
    ) {
        LibraryContent(
            state = state,
            modifier = modifier,
        )
    }
}

@Composable
internal fun LibraryContent(
    state: LibraryScreen.State,
    modifier: Modifier = Modifier,
) {
    Column(
        modifier = modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center,
    ) {
        Box(modifier = modifier.fillMaxSize()) {
            Column(
                modifier = Modifier.fillMaxSize(),
                horizontalAlignment = Alignment.CenterHorizontally,
                verticalArrangement = Arrangement.Center,
            ) {
                Text(text = "내 서재")
                Spacer(modifier = Modifier.height(16.dp))
                // 변경 부분(예시)
                when (state.userProfile) {
                    is LibraryScreen.UserProfileState.Loading -> {
                        CircularProgressIndicator()
                    }
                    is LibraryScreen.UserProfileState.Success -> {
                        Text(text = state.userProfile.nickname)
                        Spacer(modifier = Modifier.height(16.dp))
                        Text(text = state.userProfile.email)
                    }
                    is LibraryScreen.UserProfileState.Error -> {
                        Text(text = "프로필 로딩 실패")
                        Spacer(modifier = Modifier.height(16.dp))
                        Text(text = state.userProfile.message)
                    }
                }
            }
            BooketButton(
                onClick = {
                    state.eventSink(LibraryScreen.Event.OnLogoutButtonClick)
                },
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(start = 32.dp, end = 32.dp, bottom = 32.dp)
                    .height(56.dp)
                    .align(Alignment.BottomCenter),
                text = {
                    Text(
                        text = stringResource(id = R.string.logout),
                        fontSize = 18.sp,
                        style = TextStyle(
                            fontWeight = FontWeight.SemiBold,
                            fontSize = 18.sp,
                            lineHeight = 25.sp,
                        ),
                    )
                },
            )
        }
    }
}

@seoyoon513
Copy link
Copy Markdown
Contributor

seoyoon513 commented Jul 7, 2025

produceRetainedState를 사용해서 API 통신 결과(로딩/성공/실패)를 통합한게 훨씬 좋네요 👍 produceState를 직접 활용해본 경험이 없는데 이번 코드 보면서 공부가 많이 됐습니다 ㅎㅎ 요 방법으로 적용하는게 더 좋을 것 같아요

authRepository.logout()
.onSuccess {
repository.clearTokens()
authRepository.clearTokens()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

로그인/로그아웃 관련 비즈니스 로직을 DefaultAuthRepository 내부에서 처리하는건 어떻게 생각해? 개인적으로 서버통신&로컬토큰 처리까지 캡슐화하고 Presenter에서는 logout()/login()만 호출하는게 나을 것 같아

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좋은 방향 같구만

Copy link
Copy Markdown
Contributor Author

@easyhooon easyhooon Jul 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@seoyoon513 근데 관련해서 고민을 해봐야할게
repository 에서 토큰 제거 처리를 한다는 것은 현재 구현 로직상 api 성공/실패 여부와 상관없이 로그아웃을 진행한다는 의미라, 로그아웃 기능 정책이 달라지는 거거든

  1. 로그아웃 API 호출 -> 성공(서버에서 해당 토큰들을 블랙 리스트 처리) -> 클라이언트 디비내 토큰을 제거하고, 로그인 화면으로 이동 (현재의 구현 방식)
  2. 로그아웃 API 호출 -> 성공 실패 여부와 상관 없이, 클라이언트 디비내 토큰을 제거하고, 로그인 화면으로 이동(누나가 제안한 방법)

사실 로그아웃를 눌렀을때 API 호출이 실패(accessToken, refreshToken이 다 만료)하여 로그인 화면으로 이동하든, API 호출이 성공하여 로그인 화면으로 이동하든 결과만 놓고 보면 같은 것이라 기획적인 문제라고도 볼 수 있을 것 같은데(서버에서 토큰을 블랙리스트에 추가하는 여부만 다름), 이건 서버랑도 같이 얘기 나눠보면 좋을 것 같은 주제인것 같네

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오키 이건 이번주 팀세션 때 회원탈퇴랑 같이 얘기해보자

@seoyoon513
Copy link
Copy Markdown
Contributor

리뷰가 늦은 관계로 믿음의 approve 해놓겠습니다~ (기능상 문제는 없어보여요)

@easyhooon
Copy link
Copy Markdown
Contributor Author

easyhooon commented Jul 7, 2025

produceRetainedState를 사용해서 API 통신 결과(로딩/성공/실패)를 통합한게 훨씬 좋네요 👍 produceState를 직접 활용해본 경험이 없는데 이번 코드 보면서 공부가 많이 됐습니다 ㅎㅎ 요 방법으로 적용하는게 더 좋을 것 같아요

@seoyoon513 화요일에도 언급을 하겠지만, 그밖에 Retained이라는 키워드가 붙은 연산자를 Circuit에서 많이 지원하는데 (ex. rememberRetained, collectAsRetainedState, produceAsRetainedState...etc) 다 비슷하게 configuration change 상황에서도 기존 작업의 상태를 계속해서 유지한다고 보면 될것같아여

@easyhooon easyhooon merged commit a2bacb3 into develop Jul 7, 2025
8 checks passed
@easyhooon easyhooon deleted the BOOK-92-feature/#28 branch July 7, 2025 07:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BOOK-92/feat] 유저 프로필 조회 API 연동

2 participants